
#region Imported Namespaces
using System;
using System.Collections;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Data;
using System.Drawing;
using System.Runtime.Serialization;
using System.Web.UI;
using System.Web.UI.Design;
using System.Web.UI.WebControls;
#endregion

namespace PHSRAG.WebControls
{
	#region Class ExpandColumn
	/// <summary>
	/// The ExpandColumn class is used to support the notion of a row in the NestedDataGrid that may be expanded
	/// and collapsed.
	/// Note that we can't make this class into a drag-and-drop control since the base (DataGridColumn) is not a control.
	/// The implication is that users of the NestedDataGrid have to add this column by hand.
	/// </summary>
	public class ExpandColumn : System.Web.UI.WebControls.DataGridColumn
	{
		#region Constants
		private const string CollapseTextKey = "CollapseText";
		private const string ExpandTextKey = "ExpandText";
		#endregion

		#region Constructor
		/// <summary>
		/// Construct an instance of the ExpandColumn type and set the default values for the collapsed and expanded texts.
		/// </summary>
		public ExpandColumn()
		{
			// Set the default values for the collapse/expand link. Typically, this would be overridden by the page
			// via the like-named properites.

			CollapseText = "+";
			ExpandText = "-";
		}
		#endregion

		#region Properties
		/// <summary>
		/// Get/Set the text displayed when a row is collapsed. State is maintained within the ViewState.
		/// </summary>
		public string CollapseText 
		{
			get { return Convert.ToString(ViewState[CollapseTextKey]); }
			set { ViewState[CollapseTextKey] = value; }
		}

		/// <summary>
		/// Get the name associated with the expand/collapse action. Used only by the NestedDataGrid.
		/// </summary>
		internal static string CommandName
		{
			get { return "Expand_E40D817F-190E-4822-90F2-F3DE1B35A063"; }
		}

		/// <summary>
		/// Get/Set the text displayed when a row is expanded. State is maintained within the ViewState.
		/// </summary>
		public string ExpandText
		{
			get { return Convert.ToString(ViewState[ExpandTextKey]); }
			set { ViewState[ExpandTextKey] = value; }
		}
		#endregion

		#region Overrides
		/// <summary>
		/// Initialize item and alternating-item cell. A link button is created in them to support the expand/collapse
		/// activities. The state of the link button is set to indicate whether the item is expanded or collapsed.
		/// </summary>
		/// <param name="cell">The cell which is to be initialized</param>
		/// <param name="columnIndex">The column number where the cell is located</param>
		/// <param name="itemType">One of the ListItemType values</param>
		public override void InitializeCell(TableCell cell, int columnIndex, ListItemType itemType)
		{
			base.InitializeCell(cell, columnIndex, itemType);

			if ((itemType == ListItemType.Item) || (itemType == ListItemType.AlternatingItem))
			{
				LinkButton link = new LinkButton();
				link.CommandName = CommandName;
				link.Text = IsExpanded(cell) ? CollapseText : ExpandText;
				link.CausesValidation = false;

				cell.Controls.Add(link);
				cell.VerticalAlign = VerticalAlign.Top;
				cell.Width = Unit.Pixel(20);
			}
		}
		#endregion

		#region Public Methods
		/// <summary>
		/// Find out if an item is expanded or collapsed
		/// </summary>
		/// <param name="item">DataGridItem whose expanded state is to be determined</param>
		/// <returns>True if the item is expanded; false otherwise</returns>
		public bool IsExpanded(DataGridItem item)
		{
			return IsExpanded(item.Cells[0]);
		}

		/// <summary>
		/// Find out if the item that contains the specified TableCell is expanded or collapsed
		/// </summary>
		/// <param name="cell">TableCell whose containing item's expanded state is to be determined. This cell is
		/// assumed to be the one that contains the expand/collapse link button.</param>
		/// <returns>True if the item is expanded; false otherwise</returns>
		public bool IsExpanded(TableCell cell)
		{
			if (cell.Controls.Count == 0)
				return false;
			LinkButton button = cell.Controls[0] as LinkButton;
			System.Diagnostics.Trace.Assert(button != null);
			return button.Text == CollapseText;
		}

		/// <summary>
		/// Toggle the state of the link  button to indicate the expanded state of an item
		/// </summary>
		/// <param name="cell">TableCell whose containing item's expanded state is to be toggled. This cell is
		/// assumed to be the one that contains the expand/collapse link button.</param>
		public void ToggleExpandedState(TableCell cell)
		{
			LinkButton button = cell.Controls[0] as LinkButton;
			System.Diagnostics.Trace.Assert(button != null);
			button.Text = (button.Text == ExpandText) ? CollapseText : ExpandText;
		}
		#endregion
	}
	#endregion

	#region Class NestedDataGrid
	/// <summary>
	/// The NestedDataGrid is an extension of the DataGrid class which supports the notion of displaying a sub-grid when
	/// a row is expanded. The expand/collpase feature is provided in concert with the ExpandColumn class defined above.
	/// It makes sense to use this class (instead of the DataGrid) when there exists a parent/child (or some such direct)
	/// relationship between the data which is being displayed in each row of a DataGrid and another DataGrid. For example,
	/// if the parent grid displays summary, then the child grid for each summary row could display the details.
	/// </summary>
	[ToolboxData("<{0}:NestedDataGrid runat=server />")]
	public class NestedDataGrid : System.Web.UI.WebControls.DataGrid
	{
		#region Constants
		private const string ChildGridID = "ChildGrid_BCD53C93-3D9D-473b-B235-2801D7840D9F";
		private const string ExpandedItemListKey = "ExpandedItems_BCD53C93-3D9D-473b-B235-2801D7840D9F";
		#endregion

		#region Instance Variables
		private DataGrid	childGrid;
		private string[]	itemExpansionKeys;
		#endregion

		#region Constructor
		/// <summary>
		/// Initialize the default state of the object and wireup event handlers.
		/// </summary>
		public NestedDataGrid()
		{
			itemExpansionKeys = new string[PageSize];
			for (int i = 0; i < PageSize; ++i)
				itemExpansionKeys[i] = string.Format("ExpandItem_{0}", i);

			AllowPaging = true;
			PagerStyle.Visible = false;
		}
		#endregion

		#region Public Events
		/// <summary>
		/// The ItemExpandedEventHandler delegate is used to propagate the expansion/collapse of a row.
		/// </summary>
		public delegate DataSet ItemExpandedEventHandler(DataGridItem item, bool expanded);

		/// <summary>
		/// The ItemExpanded event is raised whenever a row is either expanded or collapsed.
		/// </summary>
		[Category("Action"), Description("Event raised when an item in the NestedDataGrid is expanded/collapsed")]
		public event ItemExpandedEventHandler	ItemExpanded;
		#endregion

		#region Overrides
		/// <summary>
		/// Initialize the data grid columns when the grid layout is being created. The only reason why this override is
		/// taken is to ensure that the first column in the data grid is of ExpandedColumn type.
		/// </summary>
		/// <param name="item">The data grid row that is being rendered</param>
		/// <param name="columns">Array of DataGridColumn objects for the row</param>
		protected override void InitializeItem(DataGridItem item, DataGridColumn[] columns)
		{
			// The first column MUST be an ExpandColumn.

			System.Diagnostics.Trace.Assert(columns[0] is ExpandColumn, "First column must be an ExpandColumn");
			base.InitializeItem(item, columns);
		}

		/// <summary>
		/// Handler for the event raised by the data grid when any button within the grid is clicked. We are interested
		/// in only handling the click on the expand/collpase button.
		/// </summary>
		/// <param name="e">Container of event data</param>
		protected override void OnItemCommand(DataGridCommandEventArgs e)
		{
			// CAUTION: This logic is work-in-progress.

			base.OnItemCommand(e);
			if (e.CommandName == ExpandColumn.CommandName)
			{
				// If the containing entity (typically a page) has registered an event handler for the expanded/collapsed
				// event, toggle the state of the expand column, and then ask the containing entity to provide the dataset
				// which must be bound to the parent/child grid depending on the collapse/expand state (respectively).

				if (ItemExpanded != null)
				{
					ExpandColumn ec = Columns[0] as ExpandColumn;
					ec.ToggleExpandedState(e.Item.Cells[0]);
					bool isExpanded = ec.IsExpanded(e.Item);

					ArrayList itemContextList = ViewState[ExpandedItemListKey] as ArrayList;
					if (itemContextList == null)
						itemContextList = new ArrayList();
					itemContextList.Add(new ItemContext(e.Item.Cells[1].Text, isExpanded));

					foreach (ItemContext context in itemContextList)
					{
						DataGridItem item = null;
						foreach (DataGridItem i in Items)
						{
							if (i.Cells[1].Text == context.itemKey)
							{
								item = i;
								break;
							}
						}

						DataSet dataset = ItemExpanded(item, context.isExpanded);
						if (dataset != null)
						{
							if (context.isExpanded)
								BuildChildLayout(item, dataset);
							else
								RebuildParentLayout(item, dataset);
						}
					}

					ViewState[ExpandedItemListKey] = itemContextList;
				}
			}
		}

		/// <summary>
		/// Handler for the event raised when one of the page selection elements is clicked.
		/// </summary>
		/// <param name="e">Container of event data</param>
		protected override void OnPageIndexChanged(DataGridPageChangedEventArgs e)
		{
			base.OnPageIndexChanged(e);
			// Set the scenario to have no item is expanded, selected or edited, and update the data grid to display
			// the appropriate page.

			CurrentPageIndex = e.NewPageIndex;
			SelectedIndex = -1;
			EditItemIndex = -1;
		}
		#endregion

		[Serializable()]
		private struct ItemContext
		{
			public string	itemKey;
			public bool		isExpanded;

			public ItemContext(string itemKey, bool isExpanded)
			{
				this.itemKey = itemKey;
				this.isExpanded = isExpanded;
			}
		}

		#region Private Methods
		/// <summary>
		/// Build the complete layout for the child grid.
		/// </summary>
		/// <param name="item">The item which is currently expanded</param>
		/// <param name="dataset">DataSet to use as the data source for the child grid</param>
		private void BuildChildLayout(DataGridItem item, DataSet dataset)
		{
			// The control hierarchy which will be created to host the child grid is as follows:
			//
			// New DataGridItem
			//		TableCell
			//			Table
			//				TableRow
			//					TableCell (for the spacer)
			//					TableCell (for the child grid)
			//						DataGrid (child grid)
			//
			// The type of the new DataGridItem will be the same as the row that is being expanded. This will render the
			// new row to have the same background color as the expanded row.
			// The width of the spacer cell is set to the width of the ExpandColumn. This will redner the child grid
			// to line up with rest of the columns of the expanded row.
			// Once this hierarchy is fully created, load the child grid with data.

			// IMPORTANT: It is CRUCIAL that the ID of the child grid not change between postbacks. If it did, any event
			// wireup we would have done will be lost and we will no longer be able to do things such as paging.

			DataGridItem childGridItem = new DataGridItem(0, 0, item.ItemType);
			Controls[0].Controls.AddAt(item.ItemIndex + 3, childGridItem);	// Controls[0] is the Table.

			TableCell cell = new TableCell();
			childGridItem.Cells.Add(cell);
			cell.ColumnSpan = item.Cells.Count;
			cell.Width = Unit.Percentage(100);

			Table table = new Table();
			cell.Controls.Add(table);

			TableRow childGridRow = new TableRow();
			table.Rows.Add(childGridRow);

			TableCell spacerCell = new TableCell();
			childGridRow.Cells.Add(spacerCell);
			spacerCell.Width = Columns[0].ItemStyle.Width;

			TableCell childGridCell = new TableCell();
			childGridRow.Cells.Add(childGridCell);
			
			childGrid = new DataGrid();
			childGridCell.Controls.Add(childGrid);
			childGrid.ID = ChildGridID;
			childGrid.ControlStyle.Reset();
			childGrid.CssClass = "DataGrid";
			childGrid.AlternatingItemStyle.CssClass = AlternatingItemStyle.CssClass;
			childGrid.ItemStyle.CssClass = ItemStyle.CssClass;
			childGrid.HeaderStyle.CssClass = HeaderStyle.CssClass;
			childGrid.AllowPaging = childGrid.AllowSorting = false;

			childGrid.DataSource = dataset;
			childGrid.DataBind();
		}

		/// <summary>
		/// Called to rebuild the layout of the parent grid. This is invoked only when an expanded item gets collapsed.
		/// </summary>
		/// <param name="item">The item which is currently expanded</param>
		/// <param name="dataset">DataSet to use as the data source for the parent grid</param>
		private void RebuildParentLayout(DataGridItem item, DataSet dataset)
		{
			// When an item got expanded, we would have added a new DataGridItem to the Controls collection for that item.
			// Remove that now to set the state of the item to what it used to be before the expansion. However, if we just
			// left it at that, you would notice two consecutive items of the same type (item/alternating-item). Since
			// there seems to be no quick way to 
			item = Controls[0].Controls[item.ItemIndex + 3] as DataGridItem;
			if (item != null)
				Controls[0].Controls.RemoveAt(item.ItemIndex + 3);
			DataSource = dataset;
			DataBind();
		}
		#endregion
	}
	#endregion
}

// --- EOF ---
